package com.javabi.common.text;

import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.NumberFormat;

import com.google.common.base.Throwables;

/**
 * A collection of useful methods for string manipulation.
 */
// ESCA-JAVA0136: utility, many methods allowed
public final class StringUtil {

	/**
	 * Inaccessible Constructor.
	 */
	private StringUtil() {
	}

	/**
	 * Returns the cached string value of the given object.
	 * @param object the object.
	 * @return the string.
	 */
	public static final String valueOf(Object object) {
		if (object == null) {
			return "null";
		}
		if (object instanceof Throwable) {
			return Throwables.getStackTraceAsString((Throwable) object);
		}
		return object.toString();
	}

	/**
	 * Returns true if the given text is a whole number.
	 * @param text the text.
	 * @return true if the given text is a whole number.
	 */
	public static final boolean isNumber(String text) {
		if (text.length() == 0) {
			return false;
		}
		for (int i = 0; i < text.length(); i++) {
			char c = text.charAt(i);
			if (!Character.isDigit(c)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Trim the given string.
	 * @param text the string.
	 * @return the trimmed string.
	 */
	public static final String trim(String text) {
		if (text == null || text.isEmpty()) {
			return text;
		}
		StringBuilder buffer = new StringBuilder();
		boolean first = true;
		boolean space = false;
		for (int i = 0; i < text.length(); i++) {
			char c = text.charAt(i);
			if (Character.isWhitespace(c)) {
				space = true;
			} else {
				if (space && !first) {
					buffer.append(' ');
				}
				first = false;
				space = false;
				buffer.append(c);
			}
		}
		return buffer.toString();
	}

	/**
	 * Returns a two digit string parsed from the given number.
	 * @param number the number.
	 * @return a two digit string.
	 */
	public static final String getTwoDigitString(long number) {
		if (number > 9) {
			String s = String.valueOf(number);
			if (s.length() > 2) {
				s = s.substring(s.length() - 2, s.length());
			}
			return s;
		}
		if (number >= 0 && number < 10) {
			return "0" + number;
		}
		throw new IllegalArgumentException("number=" + number);
	}

	/**
	 * Returns a two digit string parsed from the given number.
	 * @param number the number.
	 * @return a two digit string.
	 */
	public static final String getTwoDigitString(String number) {
		return getTwoDigitString(Long.parseLong(number));
	}

	/**
	 * Split the given string on the delimiter.
	 * @param toSplit the string to split.
	 * @param delimiter the delimiter.
	 * @return the split string.
	 */
	public static final String[] split(String toSplit, String delimiter) {
		if (toSplit == null || delimiter == null) {
			throw new NullPointerException();
		}
		if (delimiter.length() == 0) {
			throw new IllegalArgumentException("delimiter cannot be an empty string");
		}
		int count = count(toSplit, delimiter);
		if (count == 0) {
			return new String[] { toSplit };
		}
		String[] splitted = new String[count + 1];
		int indexStart = 0;
		int indexEnd = 0;
		for (int i = 0; i < splitted.length; i++) {
			indexEnd = toSplit.indexOf(delimiter, indexStart);
			if (indexEnd == -1) {
				indexEnd = toSplit.length();
			}
			splitted[i] = toSplit.substring(indexStart, indexEnd);
			indexStart = indexEnd + delimiter.length();
		}
		return splitted;
	}

	/**
	 * Split the given string on the delimiter.
	 * @param toSplit the string to split.
	 * @param delimiter the delimiter.
	 * @return the split string.
	 */
	public static final String[] split(String toSplit, char delimiter) {
		if (toSplit == null) {
			throw new NullPointerException();
		}
		int count = count(toSplit, delimiter);
		if (count == 0) {
			return new String[] { toSplit };
		}
		String[] splitted = new String[count + 1];
		int indexStart = 0;
		int indexEnd = 0;
		for (int i = 0; i < splitted.length; i++) {
			indexEnd = toSplit.indexOf(delimiter, indexStart);
			if (indexEnd == -1) {
				indexEnd = toSplit.length();
			}
			splitted[i] = toSplit.substring(indexStart, indexEnd);
			indexStart = indexEnd + 1;
		}
		return splitted;
	}

	/**
	 * Counts and returns the number of occurances of the given string in the string.
	 * @param countIn the string to count in.
	 * @param delimiter the delimiter string.
	 * @return the number.
	 */
	public static final int count(String countIn, String delimiter) {
		int number = 0;
		int index = 0;
		while (true) {
			index = countIn.indexOf(delimiter, index);
			if (index == -1) {
				break;
			}
			number++;
			index += delimiter.length();
		}
		return number;
	}

	/**
	 * Counts and returns the number of occurances of the given string in the string.
	 * @param countIn the string to count in.
	 * @param delimiter the delimiter string.
	 * @return the number.
	 */
	public static final int count(String countIn, char delimiter) {
		int number = 0;
		int index = 0;
		while (true) {
			index = countIn.indexOf(delimiter, index);
			if (index == -1) {
				break;
			}
			number++;
			index++;
		}
		return number;
	}

	/**
	 * Returns the index of the given substring from the given index.
	 * @param cs the character sequence to search.
	 * @param sub the substring to find the index of.
	 * @param index the index.
	 * @return the index or the substring of -1 if not found.
	 */
	public static int indexOfIgnoreCase(CharSequence cs, String sub, int index) {
		if (sub.length() == 0) {
			return index;
		}
		if (sub.length() > cs.length()) {
			return -1;
		}
		int subIndex = 0;
		for (int i = index; i < cs.length(); i++) {
			char c1 = Character.toLowerCase(cs.charAt(i));
			char c2 = Character.toLowerCase(sub.charAt(subIndex));
			if (c1 == c2) {
				subIndex++;
				if (subIndex == sub.length()) {
					return i - subIndex + 1;
				}
			} else {
				subIndex = 0;
			}
		}
		return -1;
	}

	/**
	 * Returns the index of the first whitespace from the given index.
	 * @param text the character sequence to search.
	 * @param index the index.
	 * @return the index of the whitespace or -1 if not found.
	 */
	public static final int indexOfWhitespace(CharSequence text, int index) {
		for (int i = index; i < text.length(); i++) {
			char c = text.charAt(i);
			if (Character.isWhitespace(c)) {
				return i;
			}
		}
		return -1;
	}

	public static final int indexOfWhitespace(CharSequence text) {
		return indexOfWhitespace(text, 0);
	}

	/**
	 * Returns the index of the first non-whitespace from the given index.
	 * @param cs the character sequence to search.
	 * @param index the index.
	 * @return the index of the whitespace or -1 if not found.
	 */
	public static int indexOfNonWhitespace(CharSequence cs, int index) {
		for (int i = index; i < cs.length(); i++) {
			char c = cs.charAt(i);
			if (!Character.isWhitespace(c)) {
				return i;
			}
		}
		return -1;
	}

	public static String substringEnd(String text, String from) {
		return substringEnd(text, from, true);
	}

	public static String substringEnd(String text, String from, boolean includeFrom) {
		int index = text.lastIndexOf(from);
		if (index == -1) {
			return null;
		}
		if (includeFrom) {
			return text.substring(index);
		} else {
			return text.substring(index + from.length());
		}
	}

	public static String trimEnd(String text, String from) {
		return trimEnd(text, from, true);
	}

	public static String trimEnd(String text, String from, boolean includeFrom) {
		int index = text.lastIndexOf(from);
		if (index == -1) {
			return text;
		}
		if (includeFrom) {
			return text.substring(0, index);
		} else {
			return text.substring(0, index + from.length());
		}
	}

	/**
	 * Returns true if the first character sequence starts with the second (ignoring case).
	 * @param cs1 the first character sequence.
	 * @param cs2 the second character sequence.
	 * @return true if the first character sequence starts with the second (ignoring case).
	 */
	public static final boolean startsWithIgnoreCase(CharSequence cs1, CharSequence cs2) {
		if (cs1.length() == 0 || cs2.length() == 0) {
			return false;
		}
		if (cs1.length() < cs2.length()) {
			return false;
		}
		for (int i = 0; i < cs2.length(); i++) {
			char c1 = Character.toLowerCase(cs1.charAt(i));
			char c2 = Character.toLowerCase(cs2.charAt(i));
			if (c1 != c2) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Replaces all occurances of the given string with the replacement.
	 * @param replaceIn the string to replace in.
	 * @param toReplace the string to replace.
	 * @param replaceWith the string to replace with.
	 * @return the result.
	 */
	public static String replace(String replaceIn, String toReplace, String replaceWith) {
		if (replaceIn == null || toReplace == null || replaceWith == null) {
			throw new NullPointerException();
		}
		if (replaceIn.length() == 0 || toReplace.length() == 0) {
			return replaceIn;
		}
		if (toReplace.length() > replaceIn.length()) {
			return replaceIn;
		}
		int index = replaceIn.indexOf(toReplace);
		if (index == -1) {
			return replaceIn;
		}
		int length = toReplace.length();
		StringBuilder sb = new StringBuilder(replaceIn.length());
		for (int i = 0; i < replaceIn.length(); i++) {
			if (index == i) {
				sb.append(replaceWith);
				// ESCA-JAVA0119: index increment necessary
				i += length - 1;
				index = replaceIn.indexOf(toReplace, index + length);
			} else {
				char c = replaceIn.charAt(i);
				sb.append(c);
			}
		}
		return sb.toString();
	}

	/**
	 * Inserts escaped characters into the given string.
	 * @param s the string to insert into.
	 * @param skipEscaped true to skip escaped characters.
	 * @param escape the characters to escape.
	 * @return the resulting string.
	 */
	public static final String insertEscapedChars(String s, boolean skipEscaped, char... escape) {
		if (s == null || escape == null) {
			throw new NullPointerException();
		}
		boolean found = false;
		for (int index = 0; index < s.length(); index++) {
			char c = s.charAt(index);
			if (skipEscaped && c == '\\') {
				// ESCA-JAVA0119: index increment necessary
				index++;
			} else {
				if (contains(escape, c)) {
					found = true;
					break;
				}
			}
		}
		if (!found) {
			return s;
		}
		StringBuilder b = new StringBuilder(s.length() * 2);
		for (int index = 0; index < s.length(); index++) {
			char c = s.charAt(index);
			if (contains(escape, c)) {
				b.append('\\');
			}
			b.append(s.charAt(index));
		}
		return b.toString();
	}

	/**
	 * Removes escaped characters from the given string.
	 * @param s the string to remove from.
	 * @return the resulting string.
	 */
	public static final String removeEscapedChars(String s) {
		if (s == null) {
			throw new NullPointerException();
		}
		int endIndex = s.indexOf('\\');
		if (endIndex == -1) {
			return s;
		}
		StringBuilder b = new StringBuilder();
		int startIndex = 0;
		while (true) {
			b.append(s.substring(startIndex, endIndex++));
			int tempIndex = s.indexOf('\\', endIndex + 1);
			if (tempIndex == -1) {
				break;
			}
			startIndex = endIndex;
			endIndex = tempIndex;
		}
		b.append(s.substring(endIndex, s.length()));
		return b.toString();
	}

	/**
	 * Returns true if the given character array contains the given character!
	 * @param array the character array.
	 * @param c the character.
	 * @return true if the character was found.
	 */
	public static final boolean contains(char[] array, char c) {
		if (array == null) {
			throw new NullPointerException();
		}
		for (char element : array) {
			if (element == c) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Remove the given string from another.
	 * @param removeFrom the string to remove from.
	 * @param toRemove the string to remove.
	 * @return the resulting string.
	 */
	public static final String remove(String removeFrom, String toRemove) {
		return replace(removeFrom, toRemove, "");
	}

	/**
	 * Removes substrings from a string.
	 * @param removeFrom the string to remove from.
	 * @param from the start of the substring to remove.
	 * @param to the end of the substring to remove.
	 * @return the resulting string.
	 */
	public static final String remove(String removeFrom, String from, String to) {
		if (removeFrom == null || from == null || to == null) {
			throw new NullPointerException();
		}
		if (from.length() == 0) {
			throw new IllegalArgumentException("from is empty");
		}
		if (to.length() == 0) {
			throw new IllegalArgumentException("to is empty");
		}
		int indexFrom = removeFrom.indexOf(from);
		if (indexFrom == -1) {
			return removeFrom;
		}
		int indexTo = removeFrom.indexOf(to, indexFrom + from.length());
		if (indexTo == -1) {
			return removeFrom;
		}
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < removeFrom.length(); i++) {
			if (i == indexFrom) {
				i = indexTo + to.length();
				indexFrom = removeFrom.indexOf(from, indexTo + to.length());
				if (indexFrom != -1) {
					indexTo = removeFrom.indexOf(to, indexFrom + from.length());
					if (indexTo == -1) {
						indexFrom = -1;
					}
				}
				if (i == removeFrom.length()) {
					break;
				}
			}
			char c = removeFrom.charAt(i);
			sb.append(c);
		}
		return sb.toString();
	}

	/**
	 * Removes whitespace from the given character sequence.
	 * @param cs the character sequence.
	 * @return the resulting character sequence.
	 */
	public static String removeWhitespace(CharSequence cs) {
		if (cs == null) {
			throw new NullPointerException();
		}
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < cs.length(); i++) {
			char c = cs.charAt(i);
			if (!Character.isWhitespace(c)) {
				sb.append(c);
			}
		}
		return sb.toString();
	}

	/**
	 * Strip all whitespace and convert uppercase characters.
	 * @param toStrip the text to strip.
	 * @return the stripped code.
	 */
	public static final String strip(String toStrip) {
		if (toStrip == null) {
			throw new NullPointerException();
		}
		StringBuilder stripped = new StringBuilder();
		for (int i = 0; i < toStrip.length(); i++) {
			char c = toStrip.charAt(i);
			if (Character.isLetterOrDigit(c)) {
				c = Character.toLowerCase(c);
				stripped.append(c);
			}
		}
		return stripped.toString();
	}

	/**
	 * Returns the given text in constant case (uppercase with underscores).
	 * @param text the text.
	 * @return the converted text.
	 */
	public static final String toConstantCase(String text) {
		StringBuilder builder = new StringBuilder();
		boolean uppercase = false;
		for (int i = 0; i < text.length(); i++) {
			char c = text.charAt(i);
			if (Character.isUpperCase(c) && !uppercase) {
				if (i > 0) {
					builder.append('_');
				}
				uppercase = true;
			} else {
				uppercase = false;
			}
			builder.append(Character.toUpperCase(c));
		}
		return builder.toString();
	}

	/**
	 * Returns the given text in name case.
	 * @param text the text to convert.
	 * @return the converted text.
	 */
	public static final String toNameCase(String text) {
		StringBuilder buffer = new StringBuilder();
		boolean upper = true;
		for (int k = 0; k < text.length(); k++) {
			char c = text.charAt(k);
			if (Character.isLetter(c)) {
				if (upper) {
					c = Character.toUpperCase(c);
					upper = false;
				} else {
					c = Character.toLowerCase(c);
				}
			} else {
				upper = true;
			}
			buffer.append(c);
		}
		return buffer.toString();
	}

	/**
	 * Insert commas into the given number.
	 * @param l the number.
	 * @return the string.
	 */
	public static String insertCommas(long l) {
		String s = String.valueOf(l);
		// ESCA-JAVA0076: 999 hard coded
		if (-999 < l && l < 999) {
			return s;
		}
		return insertCommas(s);
	}

	/**
	 * Insert commas into the given number.
	 * @param number the number.
	 * @return the string.
	 */
	public static String insertCommas(String number) {
		if (number.length() < 4) {
			return number;
		}
		if (number.length() == 4 && number.startsWith("-")) {
			return number;
		}
		int count = 0;
		StringBuilder sb = new StringBuilder();
		for (int i = number.length() - 1; i >= 0; i--) {
			if (count == 3) {
				sb.insert(0, ',');
				count = 1;
			} else {
				count++;
			}
			sb.insert(0, number.charAt(i));
		}
		return sb.toString();
	}

	/**
	 * Returns the given long as a formatted string.
	 * @param bytes the bytes.
	 * @param decimalPlaces the number of decimal places.
	 * @return the long.
	 */
	public static String bytes(long bytes, int decimalPlaces) {
		if (bytes >= 1000000000) {
			return bytes(bytes, decimalPlaces, 1000000000, "GB");
		}
		if (bytes >= 1000000) {
			return bytes(bytes, decimalPlaces, 1000000, "MB");
		}
		if (bytes >= 1000) {
			return bytes(bytes, decimalPlaces, 1000, "KB");
		}
		return bytes + " B";
	}

	/**
	 * Returns the given long as a formatted string.
	 * @param bytes the bytes.
	 * @param decimalPlaces the number of decimal places.
	 * @param divisor the divisor.
	 * @param postfix the postfix.
	 * @return the long.
	 */
	private static String bytes(long bytes, int decimalPlaces, long divisor, String postfix) {
		NumberFormat format = DecimalFormat.getInstance();
		format.setMaximumFractionDigits(decimalPlaces);
		StringBuffer buffer = new StringBuffer();
		format.format(((double) bytes) / divisor, buffer, new FieldPosition(0));
		buffer.append(' ');
		buffer.append(postfix);
		return buffer.toString();
	}

	public static String maxLength(String text, int maxLength) {
		if (maxLength < 0) {
			throw new IllegalArgumentException("maxLength=" + maxLength);
		}
		if (text == null || text.length() <= maxLength) {
			return text;
		}
		return text.substring(0, maxLength);
	}
}